#include <zenoui/FontData.hpp>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <cstring>
#include <iostream>
#include <string>
#include <string_view>
#include <zenoui/check_gl.hpp>
#include <zenoui/ImageIO.hpp>
#include <zenox/ziputils.hpp>
#include <zenox/unicode.hpp>
#include <zenox/minilog.hpp>
#include <zenox/home.hpp>
#include <zenox/pragma.hpp>
#define STB_TRUETYPE_IMPLEMENTATION
#define STB_TRUETYPE_STATIC
#include <zenox/push_warn.hpp>
#include <zeno3rd/stb_truetype.h> // https://github.com/nothings/stb/blob/master/stb_truetype.h
#include <zenox/pop_warn.hpp>

namespace zenoui {

#if !defined(NDEBUG) && !defined(__clang__)
ZENO_GNUC_ATTRIBUTE(optimize("O3"))
#endif
static void compress_to_1bit(unsigned char *out, unsigned char const *in, size_t n) {
    #pragma omp parallel for
    for (size_t i = 0; i < n / 8; i++) {
        unsigned char bits = 0u;
        for (size_t j = 0; j < 8; j++) {
            bits |= (in[i * 8 + j] >= 128u ? 1u : 0u) << j;
        }
        out[i] = bits;
    }
    if (n & 7) [[unlikely]] {
        unsigned char bits = 0u;
        for (size_t j = 0; j < (n & 7); j++) {
            bits |= (in[n / 8 * 8 + j] >= 128u ? 1u : 0u) << j;
        }
        out[n / 8] = bits;
    }
}

ZENO_FORCEINLINE static void stbtt_GetBakedQuad_optimized
(const stbtt_bakedchar *ZENO_RESTRICT chardata
, int pw, int ph, int char_index
, float *ZENO_RESTRICT xpos, float *ZENO_RESTRICT ypos
, stbtt_aligned_quad *ZENO_RESTRICT q
) {
   float ipw = 1.0f / pw, iph = 1.0f / ph;
   const stbtt_bakedchar *ZENO_RESTRICT b = chardata + char_index;
   int round_x = (int)std::floor((*xpos + b->xoff) + 0.5f);
   int round_y = (int)std::floor((*ypos + b->yoff) + 0.5f);

   q->x0 = round_x;
   q->y0 = round_y;
   q->x1 = round_x + b->x1 - b->x0;
   q->y1 = round_y + b->y1 - b->y0;

   q->s0 = b->x0 * ipw;
   q->t0 = b->y0 * iph;
   q->s1 = b->x1 * ipw;
   q->t1 = b->y1 * iph;

   *xpos += b->xadvance;
}

static int stbtt_BakeFontBitmap_optimized(
    unsigned char *ZENO_RESTRICT data, int offset,        // font location (use offset=0 for plain .ttf)
    float pixel_height,                                   // height of font in pixels
    unsigned char *ZENO_RESTRICT pixels, int pw, int ph,  // bitmap to be filled in
    int first_char, int num_chars,                        // characters to bake
    stbtt_bakedchar *ZENO_RESTRICT chardata)
{
    zenox::log_trace("backing font map, U+{:04X} to U+{:04X}, texture {}x{}", first_char, first_char + num_chars, pw, ph);
    /* if (num_chars <= 1024) */
    /*     return stbtt_BakeFontBitmap(data, offset, pixel_height, pixels, pw, ph, first_char, num_chars, chardata); */
    float scale;
    int x,y,bottom_y, i;
    stbtt_fontinfo f;
    f.userdata = NULL;
    if (!stbtt_InitFont(&f, data, offset))
        return -1;
    STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels
    x=y=1;
    bottom_y = 1;

    scale = stbtt_ScaleForPixelHeight(&f, pixel_height);

    struct Row {
        int x0, y0, gw, gh, g, advance, x, y;
    };
    std::vector<Row> rows(num_chars);

    #pragma omp parallel for
    for (i=0; i < num_chars; ++i) {
        int advance, lsb, x0,y0,x1,y1,gw,gh;
        int g = stbtt_FindGlyphIndex(&f, first_char + i);
        stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb);
        stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1);
        /* zenox::log_debug() << i << x1 << x0 << y1 << y0; */
        gw = x1-x0;
        gh = y1-y0;
        rows[i] = Row{x0, y0, gw, gh, g, advance, 0, 0};
    }

    Row *was_g0 = nullptr, *first_g0 = nullptr;
    for (i=0; i < num_chars; ++i) {
        Row &row = rows[i];
        if (row.g == 0) {
            if (was_g0) [[likely]] {
                row = *was_g0;
                continue;
            } else {
                first_g0 = was_g0 = &row;
            }
        } else {
            was_g0 = nullptr;
        }
        if (x + row.gw + 1 >= pw) { // advance to next row
            y = bottom_y;
            x = 1;
        }
        if (y + row.gh + 1 >= ph) [[unlikely]] { // check if it fits vertically AFTER potentially moving to next row
            zenox::log_error("font bake: {}-th doesn't fit in row: {} + {} + 1 >= {}", i, y, row.gh, ph);
            return -i;
        }
        STBTT_assert(x+row.gw < pw);
        STBTT_assert(y+row.gh < ph);
        row.x = x;
        row.y = y;
        x = x + row.gw + 1;
        bottom_y = std::max(bottom_y, y + row.gh + 1);
    }

    stbtt_bakedchar chardata_g0;
    if (first_g0) [[likely]] {
        Row &row = *first_g0;
        stbtt_MakeGlyphBitmap(&f, pixels+row.x+row.y*pw, row.gw,row.gh,pw, scale,scale, row.g);
        chardata_g0.x0 = (stbtt_int16) row.x;
        chardata_g0.y0 = (stbtt_int16) row.y;
        chardata_g0.x1 = (stbtt_int16) (row.x + row.gw);
        chardata_g0.y1 = (stbtt_int16) (row.y + row.gh);
        chardata_g0.xadvance = scale * row.advance;
        chardata_g0.xoff     = (float) row.x0;
        chardata_g0.yoff     = (float) row.y0;
    }

    #pragma omp parallel for
    for (i=0; i < num_chars; ++i) {
        Row &row = rows[i];
        if (row.g == 0) {
            chardata[i] = chardata_g0;
            continue;
        }
        stbtt_MakeGlyphBitmap(&f, pixels+row.x+row.y*pw, row.gw,row.gh,pw, scale,scale, row.g);
        chardata[i].x0 = (stbtt_int16) row.x;
        chardata[i].y0 = (stbtt_int16) row.y;
        chardata[i].x1 = (stbtt_int16) (row.x + row.gw);
        chardata[i].y1 = (stbtt_int16) (row.y + row.gh);
        chardata[i].xadvance = scale * row.advance;
        chardata[i].xoff     = (float) row.x0;
        chardata[i].yoff     = (float) row.y0;
    }
    zenox::log_trace("backing font complete, bottom_y = {}", bottom_y);
    return bottom_y;
}

struct FontData::Private {
    std::vector<stbtt_bakedchar> g_cdata; // P-IMPL idiom: don't wanna leak this type out to save compile time
};

FontData::FontData
    ( const char *font_path
    , bool enable_cache
    , bool enable_1bit
    , float font_height_
    , char32_t char_min_
    , char32_t char_max_
    )
    : font_width(font_height_ * 0.95f)
    , font_height(font_height_)
    , char_min(char_min_)
    , char_max(char_max_)
    , font_tex(0)
    , impl(std::make_unique<Private>())
{
    if (char_max <= char_min) [[unlikely]] {
        zenox::fatal_error("char_max <= char_min");
    }
    auto cache_path = std::string(font_path) + ".cache"
        + std::to_string((int)(font_height * 10)) + '-'
        + std::to_string(char_min) + '-' + std::to_string(char_max);
    auto cache_dat_path = cache_path + ".dat";
    cache_path += ".png";

    Image<unsigned char> font_bitmap;
    if (enable_cache
        && std::filesystem::exists(zenox::u8path(cache_path))
        && std::filesystem::exists(zenox::u8path(cache_dat_path))) {
        zenox::log_trace("loading cached TTF bitmap: {}", cache_path);
        font_bitmap = Image<unsigned char>(cache_path);
        font_bitmap_width = font_bitmap.sizex();
        font_bitmap_height = font_bitmap.sizey();
        zenox::log_trace("loaded cached bitmap image: {}, size {}x{}",
                         cache_path, font_bitmap_width, font_bitmap_height);
        auto cdat = zenox::gzip_file_get_binary(cache_dat_path);
        impl->g_cdata.resize(cdat.size() / sizeof(impl->g_cdata[0]));
        std::memcpy(impl->g_cdata.data(), cdat.data(), impl->g_cdata.size() * sizeof(impl->g_cdata[0]));
        zenox::log_trace("loaded cached bitmap descriptor: {}, {} bytes, {} elements",
                         cache_dat_path, cdat.size(), impl->g_cdata.size());
    } else {
        zenox::log_trace("loading TTF file: {}", font_path);
        FILE *ttf_file = zenox::u8fopen(font_path, "rb");
        if (!ttf_file) {
            zenox::fatal_error("{}: {}", font_path, std::strerror(errno));
        }
        fseek(ttf_file, 0, SEEK_END);
        long ttf_size = ftell(ttf_file);
        if (ttf_size < 0) {
            fclose(ttf_file);
            zenox::fatal_error("{}: {}", font_path, std::strerror(errno));
        }
        fseek(ttf_file, 0, SEEK_SET);
        unsigned char *ttf_buffer = (unsigned char *)malloc(ttf_size);
        if ((long)fread(ttf_buffer, 1, ttf_size, ttf_file) != ttf_size) {
            fclose(ttf_file);
            free(ttf_buffer);
            zenox::fatal_error("{}: {}", font_path, std::strerror(errno));
        }
        fclose(ttf_file);
        zenox::log_trace("loaded TTF file: {}, file size {}", font_path, ttf_size);

        constexpr int width_factor = 128;
        font_bitmap_width = (int)std::ceil(font_width) * width_factor;
        if (enable_1bit)
            font_bitmap_width = (font_bitmap_width + 7) / 8 * 8;
        font_bitmap_height = (int)std::ceil(font_height) * (((size_t)(char_max - char_min) + width_factor - 1) / width_factor) + 1;
        zenox::log_trace("expected font bitmap texture size: {}x{}", font_bitmap_width, font_bitmap_height);

        int gl_max_size = 8192;
        CHECK_GL(glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_max_size));
        zenox::log_trace("opengl maximum supported size: {}x{}", gl_max_size, gl_max_size);
        if (font_bitmap_width > gl_max_size) [[unlikely]] {
            zenox::log_warn("texture of {}x{} cannot fit into opengl size {}x{}, clamping...",
                            font_bitmap_width, font_bitmap_height, gl_max_size, gl_max_size);

            while (font_bitmap_width > gl_max_size) {
                font_bitmap_width /= 2;
                font_bitmap_height /= 2;
                font_width /= 2;
                font_height /= 2;
            }
            zenox::log_trace("final clamped font bitmap texture size: {}x{}", font_bitmap_width, font_bitmap_height);
        }

        font_bitmap = Image<unsigned char>(font_bitmap_width, font_bitmap_height);
        impl->g_cdata.resize(char_max - char_min);
        font_bitmap_height = stbtt_BakeFontBitmap_optimized(
            ttf_buffer, 0,
            font_height, // pixel height
            font_bitmap.data(), // pixel buffer
            font_bitmap_width, font_bitmap_height, // bitmap size
            char_min, char_max - char_min, // ascii characters to bake
            impl->g_cdata.data());
        if (font_bitmap_height <= 0) [[unlikely]] {
            zenox::fatal_error("failed to bake font bitmap: {}", font_path);
        }
        zenox::log_debug("actual font bitmap size: {}x{}", font_bitmap_width, font_bitmap_height);
        free(ttf_buffer);
        font_bitmap.resizey(font_bitmap_height);

        if (enable_cache) {
            try { // in case file location not writable
                zenox::log_trace("dumping baked font bitmap to cache: {}", cache_path);
                font_bitmap.save(cache_path);
                zenox::log_trace("dumping baked font descriptor to cache: {}", cache_dat_path);
                zenox::gzip_file_put_binary(cache_dat_path, impl->g_cdata.data(), impl->g_cdata.size() * sizeof(impl->g_cdata[0]));
            } catch (...) {
                zenox::log_warn("failed to dump font bitmap cache to [{}] and [{}]", cache_path, cache_dat_path);
            }
        }
    }

    // create an OpenGL texture:
    if (enable_1bit) {
        CHECK_GL(glGenTextures(1, &font_tex));
        zenox::log_trace("loading font bitmap (1-bit) texture to OpenGL texture object {} size {}x{}",
                         font_tex, font_bitmap_width, font_bitmap_height); // ~27.5MB when size=128
        CHECK_GL(glPushAttrib(GL_ALL_ATTRIB_BITS));
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, font_tex));
        CHECK_GL(glPixelStorei(GL_UNPACK_ALIGNMENT, 1));
        CHECK_GL(glPixelStorei(GL_UNPACK_LSB_FIRST, GL_TRUE));
        static const float i2amap[] = {0.0f, 1.0f};
        CHECK_GL(glPixelMapfv(GL_PIXEL_MAP_I_TO_R, 2, i2amap));
        CHECK_GL(glPixelMapfv(GL_PIXEL_MAP_I_TO_G, 2, i2amap));
        CHECK_GL(glPixelMapfv(GL_PIXEL_MAP_I_TO_B, 2, i2amap));
        CHECK_GL(glPixelMapfv(GL_PIXEL_MAP_I_TO_A, 2, i2amap));
        std::vector<unsigned char> font_bitmap_1b((font_bitmap.size() + 7) / 8);
        compress_to_1bit(font_bitmap_1b.data(), font_bitmap.data(), font_bitmap.size());
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, font_tex));
        CHECK_GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA,
                              font_bitmap_width, font_bitmap_height,
                              0, GL_COLOR_INDEX, GL_BITMAP, font_bitmap_1b.data()));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
        CHECK_GL(glGenerateMipmap(GL_TEXTURE_2D));
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, 0));
        zenox::log_trace("done loading font bitmap (1-bit) texture to OpenGL");
        CHECK_GL(glPopAttrib());
    } else {
        CHECK_GL(glGenTextures(1, &font_tex));
        zenox::log_trace("loading font bitmap (8-bit) texture to OpenGL texture object {} size {}x{}",
                         font_tex, font_bitmap_width, font_bitmap_height); // ~0.22GB when size=128
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, font_tex));
        CHECK_GL(glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA,
                              font_bitmap_width, font_bitmap_height,
                              0, GL_ALPHA, GL_UNSIGNED_BYTE, font_bitmap.data()));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR));
        CHECK_GL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST));
        CHECK_GL(glGenerateMipmap(GL_TEXTURE_2D));
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, 0));
        zenox::log_trace("done loading font bitmap (8-bit) texture to OpenGL");
    }
}

std::vector<std::vector<float>> FontData::scan_xypos_utf32(std::u32string_view text, float size) const {
    const float x0 = font_width * 4, y0 = font_height * 4;
    const float scale = size / font_height;
    std::vector<std::vector<float>> xyposlist;
    size_t i = 0;
    for (; i < text.size(); i++) {
        float x = x0, y = y0;
        std::vector<float> xposlist = {0.0f};
        float last_x1 = 0.0f;
        float sum_delta = 0.0f;
        bool has_last = false;
        zenox::log_trace("scanning X-position in UTF-32 string of length {}", text.size());
        auto append = [&] (float qx0, float qx1) {
            if (has_last) [[unlikely]] {
                xposlist.push_back((last_x1 + qx0) * 0.5f - x0);
                sum_delta += qx0 - last_x1;
            }
            last_x1 = qx1;
            has_last = true;
        };
        for (; i < text.size(); i++) {
            char32_t ch = text[i];
            if (ch == U'\n') {
                zenox::log_trace("scan found new line at {} of {}", i, text.size());
                break;
            }
            if (ch < char_min || ch >= char_max) [[unlikely]] {
                zenox::log_trace("scan found char {} out of range [{}, {}) at {} of {}",
                                 (uint32_t)ch, (uint32_t)char_min, (uint32_t)char_max, i, text.size());
                ch = U'\r';
            }
            stbtt_aligned_quad q;
            stbtt_GetBakedQuad_optimized(
                impl->g_cdata.data(),
                font_bitmap_width, font_bitmap_height,
                ch - char_min,
                &x, &y, &q);
            append(q.x0, q.x1);
        }
        auto avg_delta = sum_delta / (float)(xposlist.size() + 1);
        /* xposlist.front() += avg_delta * 0.25f; */
        append(last_x1 + avg_delta * 0.75f, 0.0f);
        /* for (auto &x: xposlist) { */
        /*     x += avg_delta * 0.25f; */
        /* } */
        xyposlist.push_back(std::move(xposlist));
    }
    if (!text.empty() && text.back() == '\n') [[unlikely]] {
        xyposlist.push_back({0.0f});
    }
    if (scale != 1.0f) [[unlikely]] {
        for (auto &poses: xyposlist) {
            for (auto &xy: poses) {
                xy *= scale;
            }
        }
    }
    return xyposlist;
}

Rect FontData::predict_size_utf32(std::u32string_view text, float size) const {
    if (text.empty()) [[unlikely]] return {};
    float xmax = -font_width * 8, ymax = -font_height * 8;
    float xmin = font_width * 8, ymin = font_height * 8;
    const float scale = size / font_height;
    const float x0 = font_width * 4, y0 = font_height * 4;
    float x, y;
    auto bake = [&] (char32_t ch) {
            stbtt_aligned_quad q;
            stbtt_GetBakedQuad_optimized(
                impl->g_cdata.data(),
                font_bitmap_width, font_bitmap_height,
                ch - char_min,
                &x, &y, &q);
            xmax = std::max(xmax, q.x0);
            ymax = std::max(ymax, q.y0);
            xmax = std::max(xmax, q.x1);
            ymax = std::max(ymax, q.y1);
            xmin = std::min(xmin, q.x0);
            ymin = std::min(ymin, q.y0);
            xmin = std::min(xmin, q.x1);
            ymin = std::min(ymin, q.y1);
            /* printf("%f %f %f %f\n", q.x0, q.y0, q.x1, q.y1); */
    };
    size_t lineno = 1;
    for (size_t i = 0; i < text.size(); i++, lineno++) {
        x = x0;
        y = y0 + lineno * font_height;
        /* if (i < text.size() && text[i] == U'\n') { */
        bake(U'\n');
        /* } */
        x = x0;
        y = y0 + lineno * font_height;
        for (; i < text.size(); i++) {
            char32_t ch = text[i];
            if (ch == U'\n') {
                zenox::log_trace("predict found new line at {} of {}", i, text.size());
                break;
            }
            if (ch < char_min || ch >= char_max) [[unlikely]] {
                zenox::log_trace("predict found char {} out of range [{}, {}) at {} of {}",
                                 (uint32_t)ch, (uint32_t)char_min, (uint32_t)char_max, i, text.size());
                ch = U'\r';
            }
            bake(ch);
        }
    }
    if (!text.empty() && text.back() == U'\n') [[unlikely]] {
        x = x0;
        y = y0 + lineno * font_height;
        bake(U'\n');
    }
    Rect rect{{(xmin - x0) * scale, (ymin - y0) * scale}, {(xmax - xmin) * scale, (ymax - ymin) * scale}};
    rect.extendMargin(scale);
    return rect;
}

std::function<void(float, float)> FontData::prepare_draw_utf32(std::u32string_view text, float size) const {
    std::vector<stbtt_aligned_quad> quads;
    quads.reserve(text.size());
    const float scale = size / font_height;
    const float x0 = font_width * 4, y0 = font_height * 4;
    float x = x0, y = y0 + font_height;
    size_t lineno = 1;
    for (size_t i = 0; i < text.size(); i++) {
        char32_t ch = text[i];
        if (ch == U'\n') {
            x = x0;
            y = y0 + ++lineno * font_height;
            continue;
        }
        if (ch < char_min || ch >= char_max) [[unlikely]] {
            ch = U'\r';
        }
        stbtt_aligned_quad &q = quads.emplace_back();
        stbtt_GetBakedQuad_optimized(
            impl->g_cdata.data(),
            font_bitmap_width, font_bitmap_height,
            ch - char_min,
            &x, &y, &q);
    }
    return [quads = std::move(quads), font_tex = font_tex, x0, y0, scale] (float x, float y) ZENO_HOT_FN {
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, font_tex));
        CHECK_GL(glPushMatrix());
        CHECK_GL(glTranslatef(x, y, 0.0f));
        CHECK_GL(glScalef(scale, scale, 1.0f));
        CHECK_GL(glTranslatef(-x0, -y0, 0.0f));
        glBegin(GL_QUADS);
        for (auto const &q: quads) {
            glTexCoord2f(q.s0, q.t0); glVertex2f(q.x0, q.y0);
            glTexCoord2f(q.s1, q.t0); glVertex2f(q.x1, q.y0);
            glTexCoord2f(q.s1, q.t1); glVertex2f(q.x1, q.y1);
            glTexCoord2f(q.s0, q.t1); glVertex2f(q.x0, q.y1);
        }
        CHECK_GL(glEnd());
        CHECK_GL(glPopMatrix());
        CHECK_GL(glBindTexture(GL_TEXTURE_2D, 0));
    };
}

void FontData::draw_utf32(float x, float y, std::u32string_view text, float size) const {
    const float scale = size / font_height;
    const float x0 = x, y0 = y;

    CHECK_GL(glBindTexture(GL_TEXTURE_2D, font_tex));
    CHECK_GL(glPushMatrix());
    CHECK_GL(glTranslatef(x0, y0, 0.0f));
    CHECK_GL(glScalef(scale, scale, 1.0f));
    CHECK_GL(glTranslatef(-x0, -y0, 0.0f));
    glBegin(GL_QUADS);

    size_t lineno = 1;
    y += font_height;
    for (size_t i = 0; i < text.size(); i++) {
        char32_t ch = text[i];
        if (ch == U'\n') {
            x = x0;
            y = y0 + ++lineno * font_height;
            continue;
        }
        if (ch < char_min || ch >= char_max) [[unlikely]] {
            ch = U'\r';
        }
        stbtt_aligned_quad q;
        stbtt_GetBakedQuad_optimized(
            impl->g_cdata.data(),
            font_bitmap_width, font_bitmap_height,
            ch - char_min,
            &x, &y, &q);
        glTexCoord2f(q.s0, q.t0); glVertex2f(q.x0, q.y0);
        glTexCoord2f(q.s1, q.t0); glVertex2f(q.x1, q.y0);
        glTexCoord2f(q.s1, q.t1); glVertex2f(q.x1, q.y1);
        glTexCoord2f(q.s0, q.t1); glVertex2f(q.x0, q.y1);
    }

    CHECK_GL(glEnd());
    CHECK_GL(glPopMatrix());
    CHECK_GL(glBindTexture(GL_TEXTURE_2D, 0));
}

FontData::~FontData() {
    if (font_tex) CHECK_GL(glDeleteTextures(1, &font_tex));
}

FontCache::FontCache() = default;
FontCache::~FontCache() = default;

/* static int font_scales[] = {4, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128}; */
/* static int font_scales[] = {16, 32, 64, 128}; */

FontData *FontCache::get_font_by_name(std::string_view name, float size) {
    /* auto scale_it = std::lower_bound(std::begin(font_scales), std::end(font_scales), (int)size); */
    /* int sizeapprox = scale_it != std::end(font_scales) ? *scale_it : 128; */
    int sizeapprox = size >= 24.0f ? 128 : 32;
    std::pair fontkey(sizeapprox, std::string(name));
    auto it = g_font_cache.find(fontkey);
    if (it == g_font_cache.end()) [[unlikely]] {
        // zeno_home() is our project root path, while zeno_home_relative() extends this path
        auto assetdir = zenox::u8path(zenox::zeno_home_relative("assets/fonts"));
        std::string_view n = name;
        if (n.empty()) [[likely]] {
            n = "default";
        }
        auto ttfpath = assetdir / zenox::u8path(std::string(n) + ".ttf");
        if (!std::filesystem::is_regular_file(assetdir)) [[likely]] {
            ttfpath = assetdir / zenox::u8path("MiSans-Regular.ttf");
            /* ttfpath = assetdir / zenox::u8path("Hack-Regular.ttf"); */
        }
        it = g_font_cache.try_emplace(fontkey
            , /*font_path=*/zenox::pathu8(ttfpath).c_str()
            , /*enable_cache=*/false
            , /*enable_1bit=*/false
            , /*font_height=*/sizeapprox
        ).first;
    }
    return &it->second;
}

}
